﻿#region Apache 2.0 License header

//   Copyright 2012 Lesley van Zijl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License. 

#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using FavourObjects.Extensions;
using FavourObjects.ForwardBuilders;

namespace FavourObjects
{
    /// <summary>
    /// Dynamically builds a type that implements the specified interface.
    /// The type that is build is able to access the target class' members through reflection.
    /// </summary>
    /// <typeparam name="TInterface">Interface that needs to be implemented.</typeparam>
    /// <typeparam name="TTargetClass">Class that is targeted by the newly created type.</typeparam>
    internal class FavourTypeBuilder<TInterface, TTargetClass>
        : IFavourTypeBuilder
        where TInterface : class
        where TTargetClass : class
    {
        /// <summary>
        /// Cache for storing MemberInfos found through reflection.
        /// </summary>
        private readonly MemberInfoCache _methodRegistry = MemberInfoCache.ActiveCache;

        /// <summary>
        /// Reference to a field in which the instance of the targetted class is stored.
        /// </summary>
        private FieldBuilder _innerObjectField;

        /// <summary>
        /// Reference to a field in which the type of the targetted class is stored.
        /// </summary>
        private FieldBuilder _innerObjectTypeField;

        #region IFavourTypeBuilder Members

        /// <summary>
        /// Starts the building of the type.
        /// </summary>
        /// <returns>The newly constructed type.</returns>
        public Type BuildType()
        {
            Type interfaceType = typeof (TInterface);

            var className = new string(interfaceType.ToString().SkipWhile((c, i) => i == 0 && c == 'I').ToArray());

            TypeBuilder typeBuilder = CreateTypeBuilder(className);

            typeBuilder.AddInterfaceImplementation(interfaceType);

            CreateConstructor(typeBuilder);

            ImplementInterface(typeBuilder, interfaceType);

            Type generatedType = typeBuilder.CreateType();

            return generatedType;
        }

        #endregion

        /// <summary>
        /// Helper method that creates the TypeBuilder needed to build the current type.
        /// </summary>
        /// <param name="className">Name of the to be generated class.</param>
        /// <returns>A newly constructed type builder.</returns>
        private static TypeBuilder CreateTypeBuilder(String className)
        {
            const string generatedAssemblyName = "GeneratedAssembly";
            const string dllName = generatedAssemblyName + ".dll";

            const AssemblyBuilderAccess access = AssemblyBuilderAccess.Run;
            var assemblyName = new AssemblyName(dllName);
            AppDomain appDomain = AppDomain.CurrentDomain;

            AssemblyBuilder asmBuilder = appDomain.DefineDynamicAssembly(assemblyName, access);

            ModuleBuilder moduleBuilder = asmBuilder.DefineDynamicModule(dllName);

            String typeName = generatedAssemblyName + "." + className;
            return moduleBuilder.DefineType(typeName, TypeAttributes.Public);
        }

        /// <summary>
        /// Emits the code needed to generate the constructor for the generated type.
        /// </summary>
        /// <param name="typeBuilder">Typebuilder used to build the current type.</param>
        private void CreateConstructor(TypeBuilder typeBuilder)
        {
            _innerObjectField = typeBuilder.DefineField("_innerObject", typeof (Object), FieldAttributes.Private);
            _innerObjectTypeField = typeBuilder.DefineField("_innerObjectType", typeof (Type), FieldAttributes.Private);

            var constructorParameters = new[] {typeof (Object)};
            ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(
                MethodAttributes.Public | MethodAttributes.RTSpecialName,
                CallingConventions.Standard, constructorParameters);

            ILGenerator iLGenerator = constructorBuilder.GetILGenerator();

            iLGenerator.Emit(OpCodes.Ldarg_0);
            iLGenerator.Emit(OpCodes.Ldarg_1);
            iLGenerator.Emit(OpCodes.Stfld, _innerObjectField);

            iLGenerator.Emit(OpCodes.Ldarg_0);
            iLGenerator.Emit(OpCodes.Ldtoken, typeof (TTargetClass));

            MethodInfo getTypeFromHandleMethod = _methodRegistry.GetMethod(typeof (Type), "GetTypeFromHandle",
                                                                           typeof (RuntimeTypeHandle));

            iLGenerator.Emit(OpCodes.Call, getTypeFromHandleMethod);
            iLGenerator.Emit(OpCodes.Stfld, _innerObjectTypeField);

            iLGenerator.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Emits the code needed to implement all members of the specified interface.
        /// </summary>
        /// <param name="typeBuilder">Typebuilder used to build the current type.</param>
        /// <param name="interfaceType">Interface that needs to be implemented</param>
        private void ImplementInterface(TypeBuilder typeBuilder, Type interfaceType)
        {
            ImplementInterfaceMethods(typeBuilder, interfaceType);
            ImplementInterfaceProperties(typeBuilder, interfaceType);

            Type[] inheritedInterfaces = interfaceType.GetInterfaces();

            foreach (Type inheritedInterface in inheritedInterfaces)
            {
                ImplementInterface(typeBuilder, inheritedInterface);
            }
        }

        #region methods

        /// <summary>
        /// Emits the code needed to implement all methods specified on the interface.
        /// </summary>
        /// <param name="typeBuilder">Typebuilder used to build the current type.</param>
        /// <param name="interfaceType">Interface that needs to be implemented</param>
        private void ImplementInterfaceMethods(TypeBuilder typeBuilder, Type interfaceType)
        {
            IEnumerable<MethodInfo> nonPropertyMethods =
                interfaceType.GetMethods().Where(a => !a.Attributes.HasFlag(MethodAttributes.SpecialName));

            foreach (MethodInfo method in nonPropertyMethods)
            {
                const MethodAttributes attributes = MethodAttributes.Public |
                                                    MethodAttributes.HideBySig |
                                                    MethodAttributes.Virtual;

                MethodBuilder methodBuilder = typeBuilder.DefineMethod(method.Name, attributes);

                ForwardMethodBuilder<TTargetClass> forwardMethodBuilder =
                    typeBuilder.CreateForwardMethodBuilder<TTargetClass>(methodBuilder, method, _innerObjectField);
                MethodInfo forwardMethod = forwardMethodBuilder.CreateMethod();

                typeBuilder.DefineMethodOverride(forwardMethod, method);
            }
        }

        #endregion

        #region properties

        /// <summary>
        /// 
        /// </summary>
        /// <param name="typeBuilder">Typebuilder used to build the current type.</param>
        /// <param name="interfaceType">Interface that needs to be implemented</param>
        private void ImplementInterfaceProperties(TypeBuilder typeBuilder, Type interfaceType)
        {
            PropertyInfo[] propertyMethods = interfaceType.GetProperties();

            foreach (PropertyInfo property in propertyMethods)
            {
                Type targetClassType = typeof (TTargetClass);
                if (typeof (TTargetClass).HasField(property.Name))
                {
                    FieldInfo field =  targetClassType.GetField(property.Name, BindingFlags.NonPublic | BindingFlags.Public |
                                                                               BindingFlags.Static    | BindingFlags.Instance);
                    ImplementInterfacePropertyToField(typeBuilder, field, property);
                }
                else
                {
                    ImplementInterfacePropertyToProperty(typeBuilder, property);
                }
            }
        }

        /// <summary>
        /// Emits the code needed to implement a property that targets a field on the target class.
        /// </summary>
        /// <param name="typeBuilder">Typebuilder used to build the current type.</param>
        /// <param name="targetField">Reference to the target field.</param>
        /// <param name="propertyOnInterface">The property that is specified on the interface.</param>
        private void ImplementInterfacePropertyToField(TypeBuilder typeBuilder, FieldInfo targetField, PropertyInfo propertyOnInterface)
        {
            Type fieldType = targetField.FieldType;

            const PropertyAttributes attributes = PropertyAttributes.HasDefault;

            PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(targetField.Name, attributes, fieldType, null);
            ForwardPropertyToFieldBuilder<TTargetClass> builder =
                typeBuilder.CreateForwardPropertyToFieldBuilder<TTargetClass>(propertyBuilder, targetField,
                                                                              _innerObjectField);

            MethodBuilder setMethod = builder.CreateSetMethod();
            typeBuilder.DefineMethodOverride(setMethod, propertyOnInterface.GetSetMethod());

            MethodBuilder getMethod = builder.CreateGetMethod();
            typeBuilder.DefineMethodOverride(getMethod, propertyOnInterface.GetGetMethod());

        }

        /// <summary>
        /// Emits the code needed to implement a property that targets a property on the target class.
        /// </summary>
        /// <param name="typeBuilder">Typebuilder used to build the current type.</param>
        /// <param name="targetProperty">Reference to the target property.</param>
        private void ImplementInterfacePropertyToProperty(TypeBuilder typeBuilder, PropertyInfo targetProperty)
        {
            Type propertyType = targetProperty.PropertyType;

            const PropertyAttributes attributes = PropertyAttributes.HasDefault;

            PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(targetProperty.Name, attributes, propertyType, null);
            ForwardPropertyToPropertyBuilder<TTargetClass> builder =
                typeBuilder.CreateForwardPropertyToPropertyBuilder<TTargetClass>(propertyBuilder, targetProperty,
                                                                                 _innerObjectField);

            if (targetProperty.CanWrite)
            {
                MethodInfo setMethod = builder.CreateSetMethod();
                typeBuilder.DefineMethodOverride(setMethod, targetProperty.GetSetMethod());
            }
            if (targetProperty.CanRead)
            {
                MethodInfo getMethod = builder.CreateGetMethod();
                typeBuilder.DefineMethodOverride(getMethod, targetProperty.GetGetMethod());
            }
        }

        #endregion
    }
}